﻿/*
 * Copyright (c) 2010 Code Owls LLC
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
 * associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
 * subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial 
 * portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
*/
using System;
using System.Configuration;
using System.Management.Automation;
using System.Reflection;
using System.Web.Security;

namespace ASPNETMembership
{
    /// <summary>
    /// Implements the drive feature for the provider <see cref="Provider"/>.  Initializes the <see cref="Membership"/> framework for use.
    /// </summary>
    /// <remarks>
    /// The <see cref="Membership" /> framework relies on special sections of the app.config file for configuration information.  Since no 
    /// app.config is available to a typical PowerShell session, special measures must be taken to initialize the <see cref="Membership"/> class for use.
    /// </remarks>
    public class MembershipDriveInfo : PSDriveInfo
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="MembershipDriveInfo"/> class.
        /// </summary>
        /// <seealso cref="Provider.NewDrive(System.Management.Automation.PSDriveInfo)"/>
        /// <param name="driveInfo">The drive info object.</param>
        /// <param name="parameters">The dynamic drive parameters containing membership configuration.</param>
        public MembershipDriveInfo( PSDriveInfo driveInfo, DriveParameters parameters )
            : base( driveInfo )
        {
            var connectionStringName = SetProviderConnectionString(parameters);            
            var provider = InitializeSqlProvider(parameters, connectionStringName);

            UpdateMembershipInternals( provider );
        }

        /// <summary>
        /// Initializes a new SQL membership provider object
        /// </summary>
        /// <param name="parameters">The membership configuration parameters.</param>
        /// <param name="connectionStringName">Name of the connection string to use.  
        /// This connection string must exist in the <see cref="ConfigurationManager.ConnectionStrings"/>.</param>
        /// <returns></returns>
        private static MembershipProvider InitializeSqlProvider(DriveParameters parameters, string connectionStringName)
        {
            var provider = new SqlMembershipProvider();
            var nvc = new System.Collections.Specialized.NameValueCollection
                          {
                              { "connectionStringName", connectionStringName },
                              { "enablePasswordRetrieval", parameters.EnablePasswordRetrieval.ToString() },
                              { "enablePasswordReset", parameters.EnablePasswordReset.ToString() },
                              { "requiresQuestionAndAnswer", parameters.RequiresQuestionAndAnswer.ToString() },
                              { "requiresUniqueEmail", parameters.RequiresUniqueEmail.ToString() },
                              { "passwordFormat", parameters.PasswordFormat.ToString() },
                              { "maxInvalidPasswordAttempts", parameters.MaxInvalidPasswordAttempts.ToString() },
                              { "minRequiredPasswordLength", parameters.MinRequiredPasswordLength.ToString() },
                              { "minRequiredNonalphanumericCharacters", parameters.MinRequiredNonalphanumericCharacters.ToString() },
                              { "passwordAttemptWindow", parameters.PasswordAttemptWindow.ToString() },
                              { "passwordStrengthRegularExpression", parameters.PasswordStrengthRegularExpression },
                              { "applicationName", parameters.ApplicationName },                
                          };

            const string providerName = "AspNetSqlMembershipProvider";
            provider.Initialize( providerName, nvc );
            return provider;
        }

        /// <summary>
        /// Updates some private <see cref="Membership"/> properties to enable the use of ASP.NET Membership features without 
        /// having an app.config section available.
        /// </summary>
        /// <param name="defaultMembershipProvider">The provider to use as the default membership provider.</param>
        private static void UpdateMembershipInternals(MembershipProvider defaultMembershipProvider)
        {
            var type = typeof (Membership);
            var flags = BindingFlags.Static | BindingFlags.NonPublic;

            var fieldInfo = type.GetField("s_Provider",flags );

            fieldInfo.SetValue( null, defaultMembershipProvider );

            var coll = new MembershipProviderCollection
                           {
                               defaultMembershipProvider
                           };

            fieldInfo = type.GetField( "s_Providers", flags );
            fieldInfo.SetValue( null, coll );

            fieldInfo = type.GetField( "s_Initialized", flags );
            fieldInfo.SetValue( null, true );

            fieldInfo = type.GetField( "s_HashAlgorithmType", flags);
            fieldInfo.SetValue( null, "SHA1" );

            fieldInfo = type.GetField( "s_HashAlgorithmFromConfig", flags );
            fieldInfo.SetValue( null, false );
        }

        /// <summary>
        /// Sets the provider connection string by modifying private members of <see cref="ConfigurationManager.ConnectionStrings"/>.  
        /// See http://www.beefycode.com/post/Creating-a-PowerShell-Provider-pt-1-Creating-the-Drive.aspx and
        /// http://david.gardiner.net.au/2008/09/programmatically-setting.html.  
        /// </summary>
        /// <param name="parameters">The parameters.</param>
        /// <returns></returns>
        private static string SetProviderConnectionString(DriveParameters parameters)
        {
            var connectionStrings = ConfigurationManager.ConnectionStrings;

            var fieldInfo = typeof (ConfigurationElementCollection)
                .GetField("bReadOnly",
                          System.Reflection.BindingFlags.Instance | 
                          System.Reflection.BindingFlags.NonPublic
                );

            fieldInfo.SetValue( connectionStrings, false );

            // *** add the connection string from the supplied parameters to the 
            //  ConfigurationManager.ConnectionStrings catalog
            var connectionString = parameters.ConnectionString;
            var connectionStringName = Guid.NewGuid().ToString("N");;
            
            connectionStrings.Add(
                new ConnectionStringSettings(
                    connectionStringName,
                    connectionString
                    )
                );
            return connectionStringName;
        }
    }
}
